package eztools

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"

	"golang.org/x/crypto/ssh/terminal"
)

const (
	formatComp, formatMinus = "20060102", "2006-01-02"
	// LogFlagDate logs with date
	LogFlagDate = log.Ldate
	// LogFlagTime logs with time
	LogFlagTime = log.Ltime
	// LogFlagDateNTime logs with date and time
	LogFlagDateNTime = log.LstdFlags
	// LogFlagFile logs with file name
	LogFlagFile = log.Lshortfile
	// LogFlagDateTimeNFile logs with date, time and file name
	LogFlagDateTimeNFile = LogFlagDateNTime | LogFlagFile
)

var (
	// Debugging marks debugging mode
	Debugging bool // whether more debug procedures
	// Verbose marse debugging output level
	Verbose = 0
	// ChoiceNotification is printed when user needs to choose from choices
	ChoiceNotification = "Your choice is: "
	defaults           bool // whether no more confirmations asked
	logger             *log.Logger
	loggingTime        bool
	logFunc            func(...any)
)

// InitLogger opens log file
func InitLogger(out io.Writer) error {
	if out == nil {
		return ErrInvalidInput
	}
	loggingTime = false
	logger = log.New(out, "", 0)
	if logger == nil {
		return ErrNoValidResults
	}
	return nil
}

// SetLogFlags sets logging with LogFlag*
func SetLogFlags(f int) {
	if logger != nil {
		if f&LogFlagDateNTime != 0 {
			loggingTime = true
		} else {
			loggingTime = false
		}
		logger.SetFlags(f)
	}
}

// SetLogFunc sets function for all Log*()
func SetLogFunc(f func(...any)) {
	logFunc = f
}

// GetCallerLog returns caller string for Log*
func GetCallerLog() string {
	ptr, _, _, ok := runtime.Caller(4)
	if !ok {
		return ""
	}
	frames := runtime.CallersFrames([]uintptr{ptr})
	frame, more := frames.Next()
	var str string
	if more {
		frame, _ := frames.Next()
		str = frame.Function + ":" + strconv.Itoa(frame.Line) + "\n"
	}
	return str + frame.Function + ":" + strconv.Itoa(frame.Line)
}

// GetCaller returns caller function:line
// Parameter: 1 for immediate caller
func GetCaller(skip int) string {
	ptr, _, _, ok := runtime.Caller(skip)
	if !ok {
		return ""
	}
	frames := runtime.CallersFrames([]uintptr{ptr})
	frame, _ := frames.Next()
	return frame.Function + ":" + strconv.Itoa(frame.Line)
}

// logOrPrint logs it
// Parameter print2 = also shown on screen
func logOrPrint(print2 bool, out ...any) {
	//log.Print(logFunc, logger, "::", out)
	if logFunc != nil {
		logFunc(out)
		return
	}
	if logger != nil {
		logger.Println(out...)
	} else {
		log.Println(out...)
	}
	if print2 {
		fmt.Println(out...)
	}
}

// Log logs it
func Log(out ...any) {
	logOrPrint(false, out...)
}

// LogPrint logs and prints it
func LogPrint(out ...any) {
	logOrPrint(true, out...)
}

// LogWtTime logs a string with time
func LogWtTime(out ...any) {
	if logger != nil && loggingTime {
		Log(out...)
	} else {
		logOrPrint(false, time.Now().String(), out)
	}
}

// LogPrintWtTime logs and prints a string with time
func LogPrintWtTime(out ...any) {
	if logger != nil && loggingTime {
		LogPrint(out...)
	} else {
		logOrPrint(true, time.Now().String(), out)
	}
}

// LogFatal logs and prints it and exits
func LogFatal(out ...any) {
	if logFunc != nil {
		logFunc(out)
		os.Exit(1)
		return
	}
	if logger != nil {
		fmt.Println(out...)
		logger.Fatalln(out...)
	} else {
		log.Fatalln(out...)
	}
}

// ShowStrln prints anything with a line break
func ShowStrln(ps ...any) {
	if logFunc != nil {
		logFunc(ps...)
		return
	}
	fmt.Println(ps...)
}

// ShowStr prints anything with no line breaks
func ShowStr(ps ...any) {
	if logFunc != nil {
		logFunc(ps...)
		return
	}
	fmt.Print(ps...)
}

// ShowArrln prints a slice in one line with a line break
func ShowArrln(arr []string) {
	if logFunc != nil {
		logFunc(arr)
		return
	}
	for _, i := range arr {
		fmt.Print("\"", i, "\", ")
	}
	fmt.Print("\n")
}

// ShowByteln prints byte slice as string with a line break
func ShowByteln(ps []byte) {
	ShowStrln(string(ps[:]))
}

// ShowSthln prints anything with struct names and a line break
func ShowSthln(sth any) {
	if logFunc != nil {
		logFunc(sth)
		return
	}
	fmt.Printf("%#v\n", sth)
}

// ShowWtFmt prints with format string like printf
func ShowWtFmt(fs string, sth ...any) {
	if logFunc != nil {
		logFunc(sth...)
		return
	}
	fmt.Printf(fs, sth...)
}

// PromptStr prompts user and get input
func PromptStr(ps string) string {
	fmt.Print(ps + ":")
	in := bufio.NewScanner(os.Stdin)
	in.Scan()
	return in.Text()
}

// PromptPwd prompts user and get password
func PromptPwd(ps string) string {
	fmt.Print(ps + ":")
	pwd, err := terminal.ReadPassword(int(syscall.Stdin))
	fmt.Print("\n")
	if err != nil {
		return ""
	}
	return string(pwd)
}

// PromptIntStr prompts user and gets two inputs
// Return values. zero values are default
func PromptIntStr(pi string, ps string) (i int, s string) {
	fmt.Printf("%s %s:", pi, ps)
	fmt.Scanf("%d %s", &i, &s)
	//to exhaust the buffer
	bufio.NewScanner(os.Stdin).Scan()
	return
}

// PromptInt prompts user and gets input
// Return values. zero values are default
func PromptInt(pi string) (res int, err error) {
	res, err = strconv.Atoi(PromptStr(pi))
	return
}

// ChkCfmNPrompt checks defaults and return false only when user replied exception
// program exits when user replied 'q' or 'e'
// no more confirmations when user replied 'a' or 'c'
// verbose set when user replied a number, in which case the prompt will show again
//
//	All answers taken as lowercase
func ChkCfmNPrompt(noti, exception string) bool {
	if defaults {
		return true
	}
	quitCode := "q"
	confCode := "a"
	switch exception {
	case "q":
		quitCode = "e"
	case "a":
		confCode = "e"
	}
	val := PromptStr(noti + "?(any number=reset verbose level and ask again/" +
		quitCode + "=quit program/" + confCode +
		"=defaults to all confirmations/" + exception + "/...)")
	switch strings.ToLower(val) {
	case quitCode:
		LogFatal("Quiting")
	case confCode:
		defaults = true
	case exception:
		return false
	default:
		if v, err := strconv.Atoi(val); err == nil {
			Verbose = v
			return ChkCfmNPrompt(noti, exception)
		}
	}
	return true
}

// ChooseStringsWtIDs is for general usage to
// ask user to choose from a slice or anything
// parameters.
//
//	fL=quantity of elements
//	fI=get index to match user's input
//	fV=get message to show for each index
//	notif=notification string for user
func ChooseStringsWtIDs(fL func() int, fI func(int) int,
	fV func(int) string, notif string) (res int) {
	len := fL()
	if len < 1 {
		return InvalidID
	}
	for i := 0; i < len; i++ {
		fmt.Printf("%d: %s\n", fI(i), fV(i))
	}
	res, err := PromptInt(notif)
	if err == nil {
		//check for invalid input
		for i := 0; i < len; i++ {
			if fI(i) == res {
				return
			}
		}
	}
	return InvalidID
}

// ChooseInts asks user to choose from a slice
// Parameters. arr[][0]=id. arr[][1]=string
func ChooseInts(arr [][]string, notif string) (id int) {
	return ChooseStringsWtIDs(
		func() int {
			return len(arr)
		},
		func(i int) int {
			ret, err := strconv.Atoi(arr[i][0])
			if err != nil {
				return InvalidID
			}
			return ret
		},
		func(i int) string {
			return arr[i][1]
		},
		notif)
}

// ChooseMaps asks user to choose from a slice of map of string to string
// parameters: slice.
//
//	separator between two piece of information.
//	index(-es) into the map (to be contacted) to be information of each item,
//		or separators between two indexes.
//
// example: (c, ";", "a", "c") with
//
//	c	index	name	value
//		0	a	A
//		0	c	C
//		1	a	B
//		1	c	D
//	will print
//		0: A;C
//		1: B;D
func ChooseMaps(choices []map[string]string, sep string, indI ...string) int {
	if len(choices) < 1 || len(indI) < 1 {
		return InvalidID
	}
	for n, v := range choices {
		fmt.Printf("%d: ", n)
		for i, j := range indI {
			if len(indI) == (i - 1) {
				sep = "\n"
			}
			fmt.Printf("%s%s", v[j], sep)
		}
	}
	fmt.Print(ChoiceNotification)
	var str string
	fmt.Scanln(&str)
	if len(str) < 1 {
		return InvalidID
	}
	res, err := strconv.Atoi(str)
	if err != nil {
		return InvalidID
	}
	return res
}

// ChooseStrings asks user to choose from a slice
// return values: index (InvalidID if not a valid one) and string
func ChooseStrings(choices []string) (int, string) {
	if len(choices) > 0 {
		for i, v := range choices {
			fmt.Printf("%d: %s\n", i, v)
		}
	}
	fmt.Print(ChoiceNotification)
	var str string
	fmt.Scanln(&str)
	if len(str) < 1 {
		return InvalidID, str
	}
	res, err := strconv.Atoi(str)
	if err != nil {
		return InvalidID, str
	}
	//check for invalid input
	if res >= 0 && res < len(choices) {
		return res, choices[res]
	}
	return InvalidID, str
}

// GetDate asks user to input a date string
func GetDate(info string) string {
	fmt.Print(info + "date such as " + formatComp + ": ")
	var res string
	fmt.Scanln(&res)
	t, err := time.Parse(formatComp, res)
	if err == nil {
		return t.Format(formatComp)
	}
	return "NULL"
}

// TranDate removes minuses from date string
// return current date if empty string as param
func TranDate(date string) string {
	if len(date) < 1 {
		return time.Now().Format(formatComp)
	}
	t, err := time.Parse(formatMinus, date)
	if err == nil {
		return t.Format(formatComp)
	}
	return ""
}

// DiffDate = dateN - dateO
// return values: difference in days
func DiffDate(dateO, dateN string) (diff int, err error) {
	timeO, err := time.Parse(formatMinus, dateO)
	if err != nil {
		timeO, err = time.Parse(formatComp, dateO)
		if err != nil {
			return
		}
	}
	timeN, err := time.Parse(formatMinus, dateN)
	if err != nil {
		timeN, err = time.Parse(formatComp, dateN)
		if err != nil {
			return
		}
	}
	d := timeN.Sub(timeO)
	return int(d.Hours()) / 24, nil
}

// TranSize shows the number as file size format
// input params: b=number; precision=how many number to keep lower than point;
//
//	space=whether a space is put between number and unit
//
// copied from https://programming.guide/go/formatting-byte-size-to-human-readable-format.html
func TranSize(b int64, precision int, space bool) string {
	const unit = 1000
	var spaceChar string
	if space {
		spaceChar = " "
	}
	if b < unit {
		return fmt.Sprintf("%d"+spaceChar+"B", b)
	}
	div, exp := int64(unit), 0
	for n := b / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%."+strconv.Itoa(precision)+"f"+spaceChar+"%cB",
		float64(b)/float64(div), "kMGTPE"[exp])
}
